/**
 * Copyright 2009 Adam Ruggles.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.sapien.service;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.List;
import java.util.Random;

import org.apache.commons.codec.DecoderException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.sapien.dao.PersistentLoginDAO;
import com.google.code.sapien.dao.RoleDAO;
import com.google.code.sapien.dao.UserDAO;
import com.google.code.sapien.exception.ServiceException;
import com.google.code.sapien.model.PersistentLogin;
import com.google.code.sapien.model.User;
import com.google.code.sapien.util.SecurityUtils;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * Default implementation of the {@link com.google.code.sapien.service.UserService}.
 * @author Adam
 * @version $Id: UserSapienService.java 25 2009-05-26 04:23:01Z a.ruggles $
 * 
 * Created on Feb 8, 2009 at 1:59:44 PM 
 */
public class UserSapienService implements UserService {
	/**
     * The <code>Logger</code> is used by the application to generate a log messages.
     */
    private static final Logger LOG = LoggerFactory.getLogger(UserSapienService.class);

	/**
	 * Encryption Key used to encrypt messages.
	 */
	private final String encryptionKey;

	/**
	 * The persistent login data access object.
	 */
	private final PersistentLoginDAO persistentLoginDAO;

	/**
	 * Random number generator. 
	 */
	private final Random random;

	/**
	 * The role data access object.
	 */
	private final RoleDAO roleDAO;

	/**
	 * The user data access object.
	 */
	private final UserDAO userDAO;

	/**
	 * Constructs a user sapien service.
	 * @param userDAO The user data access object.
	 */
	@Inject
	public UserSapienService(final UserDAO userDAO, final PersistentLoginDAO persistentLoginDAO,
			final RoleDAO roleDAO, @Named("sapien.key") final String key) {
		this.userDAO = userDAO;
		this.persistentLoginDAO = persistentLoginDAO;
		this.roleDAO = roleDAO;
		this.encryptionKey = key;
		this.random = new SecureRandom();
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.EntityService#add(java.lang.Object, com.google.code.sapien.model.User)
	 */
	public void add(final User entity, final User creator) {
		final Date timestamp = new Date();
		entity.setCreated(timestamp);
		entity.setModified(timestamp);
		final String password = entity.getPassword();
		entity.setPassword(SecurityUtils.hash(password));
		entity.setActive(Boolean.TRUE); // TODO default for active until a new rule system is in place.
		entity.getRoles().addAll(roleDAO.listForNewUsers());
		userDAO.add(entity);
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#addPersistentLogin(com.google.code.sapien.model.User)
	 */
	public String addPersistentLogin(final User user) throws ServiceException {
		BigInteger token = new BigInteger(128, random);
		PersistentLogin persistentLogin = new PersistentLogin(user, token.toString(16));
		persistentLoginDAO.add(persistentLogin);
		StringBuilder key = new StringBuilder();
		key.append("id=").append(user.getId()).append("&")
			.append("username=").append(user.getUsername()).append("&")
			.append("token=").append(persistentLogin.getToken());
		try {
			return SecurityUtils.encrypt(key.toString(), encryptionKey);
		} catch (UnsupportedEncodingException e) {
			throw new ServiceException("Unsupported Encoding Exception when persisting a login.", e);
		} catch (GeneralSecurityException e) {
			throw new ServiceException("General Security Exception when persisting a login.", e);
		}
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#changePassword(com.google.code.sapien.model.User,
	 * java.lang.String)
	 */
	public void changePassword(final User user, final String plain) {
		final User persistentUser = userDAO.load(user.getId());
		persistentUser.setPassword(SecurityUtils.hash(plain));
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.EntityService#get(java.io.Serializable)
	 */
	public User get(final Serializable id) {
		return userDAO.get(id);
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#getByEmail(java.lang.String)
	 */
	public User getByEmail(final String email) {
		return userDAO.getByEmail(email);
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#getByUsername(java.lang.String)
	 */
	public User getByUsername(final String username) {
		return userDAO.getByUsername(username);
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#getPersistentLogin(java.lang.String)
	 */
	public User getPersistentLogin(final String encryptedMessage) throws ServiceException {
		String message = null;
		User user = null;
		try {
			message = SecurityUtils.decrypt(encryptedMessage, encryptionKey);
		} catch (UnsupportedEncodingException e) {
			throw new ServiceException("Unsupported Encoding Exception when decrypting the token.", e);
		} catch (GeneralSecurityException e) {
			throw new ServiceException("General Security Exception when decrypting the token.", e);
		} catch (DecoderException e) {
			throw new ServiceException("Decoder Exception when decrypting the token.", e);
		}
		// See persistLogin for the message format.
		LOG.debug("Unencrypted message is {}", message);
		String[] parts = message.split("&");
		if (parts.length == 3) {
			LOG.debug("Part 1 = {}", parts[0]);
			LOG.debug("Part 2 = {}", parts[1]);
			LOG.debug("Part 3 = {}", parts[2]);
			Long id = Long.valueOf(parts[0].substring(3));
			String token = parts[2].substring(6);
			user = userDAO.load(id);
			PersistentLogin persistentLogin = persistentLoginDAO.get(user, token);
			if (persistentLogin == null) {
				// An invalid persistent login has been received, we need to remove all other persistent login
				// instances for this user.
				LOG.warn("An invalid persistent login has been received for user [{}]", user);
				List<PersistentLogin> logins = persistentLoginDAO.list(user);
				for (PersistentLogin login : logins) {
					persistentLoginDAO.remove(login);
				}
				// Set the user to null so the login fails.
				user = null;
			} else {
				// The persistent login was successful, remove this instance.
				persistentLoginDAO.remove(persistentLogin);
			}
		}
		return user;
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.EntityService#remove(java.lang.Object, com.google.code.sapien.model.User)
	 */
	public void remove(final User entity, final User remover) {
		final User entityToRemove = userDAO.load(entity.getId());
		userDAO.remove(entityToRemove);
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#removePersistLogin(java.lang.String)
	 */
	public void removePersistentLogin(final String encryptedMessage) throws ServiceException {
		String message = null;
		User user = null;
		try {
			message = SecurityUtils.decrypt(encryptedMessage, encryptionKey);
		} catch (UnsupportedEncodingException e) {
			throw new ServiceException("Unsupported Encoding Exception when decrypting the token.", e);
		} catch (GeneralSecurityException e) {
			throw new ServiceException("General Security Exception when decrypting the token.", e);
		} catch (DecoderException e) {
			throw new ServiceException("Decoder Exception when decrypting the token.", e);
		}
		// See persistLogin for the message format.
		String[] parts = message.split("&");
		if (parts.length == 3) {
			Long id = Long.valueOf(parts[0].substring(3));
			String token = parts[2].substring(6);
			user = userDAO.load(id);
			PersistentLogin persistentLogin = persistentLoginDAO.get(user, token);
			if (persistentLogin == null) {
				// An invalid persistent login has been received, we need to remove all other persistent login
				// instances for this user.
				LOG.warn("An invalid persistent login has been received for user [{}]", user);
				List<PersistentLogin> logins = persistentLoginDAO.list(user);
				for (PersistentLogin login : logins) {
					persistentLoginDAO.remove(login);
				}
				// Set the user to null so the login fails.
				user = null;
			} else {
				// The persistent login was successful, remove this instance.
				persistentLoginDAO.remove(persistentLogin);
			}
		}
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.EntityService#update(java.lang.Object, com.google.code.sapien.model.User)
	 */
	public User update(final User entity, final User updator) {
		final Date timestamp = new Date();
		final User persistentUser = userDAO.load(entity.getId());
		persistentUser.setModified(timestamp);
		if (entity.getActive() != null) {
			persistentUser.setActive(entity.getActive());
		}
		if (entity.getEmail() != null) {
			persistentUser.setEmail(entity.getEmail());
		}
		if (entity.getUsername() != null) {
			persistentUser.setUsername(entity.getUsername());
		}
		return persistentUser;
	}

	/**
	 * {@inheritDoc}
	 * @see com.google.code.sapien.service.UserService#validate(java.lang.String, java.lang.String)
	 */
	public User validate(final String username, final String password) {
		if (username == null || password == null) {
			return null;
		}
		final User user = userDAO.getByUsername(username);
		if (user != null) {
			if (!SecurityUtils.evaluate(password, user.getPassword())) {
				return null;
			}
		}
		return user;
	}
}
